package metric.gui.swt.explorer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;

import metric.core.ReportDefinition;
import metric.core.ReportDefinitionRepository;
import metric.core.exception.ReportException;
import metric.core.model.HistoryMetricData;
import metric.core.report.Report;
import metric.core.report.ReportFactory;
import metric.core.util.StringUtils;
import metric.core.vocabulary.SupportedFileType;
import metric.gui.swt.core.dialog.OpenDialog;
import metric.gui.swt.core.threading.ThreadedDefinitionProcessor;
import metric.gui.swt.core.util.SWTUtils;
import metric.gui.swt.core.vocabulary.GUI;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.custom.TableEditor;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.List;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Widget;

/**
 * Represents the left composite in this GUI. This includes the Versions list,
 * and all reporting features.
 * 
 * This composite can return the currently selected report, currently selected
 * report configuration and report arguments.
 * 
 * Is responsible for handling any listener events generated by components in
 * this class.
 * 
 * @author Joshua Hayes,Swinburne University (ICT),2007
 */
public class LeftComposite extends Composite implements SelectionListener
{
	private Logger logger; 
	private List lVersions;
	private Combo cReportSelection, cReportConfig;
	private Button bReportBrowse, bReset;
	private JSeatExplorer parent;
	private Shell shell;
	private Table table;
	// private TableEditor tableEditor;

	private HashMap<String, Object> reportConfigMap;
	private ArrayList<String[]> processedMetrics;

	private TableHandler tableHandler;

	public LeftComposite(JSeatExplorer parent, final Shell shell, int type, Logger logger)
	{
		super(shell, type);
		// Set layout data.
		setLayout(new GridLayout());
		setLayoutData(SWTUtils.createGridData(GridData.FILL_VERTICAL,
				SWT.DEFAULT, SWT.DEFAULT, SWT.TOP));

		this.parent = parent;
		this.shell = shell;
		this.processedMetrics = new ArrayList<String[]>();
		this.tableHandler = new TableHandler();
		this.logger = logger;

		// Version grid data
		GridData versionGridData = SWTUtils.createGridData(GridData.FILL_BOTH,
				shell.getSize().y / 2, SWT.DEFAULT, SWT.TOP);
		Group versionGroup = SWTUtils.createGroup(this, SWT.NONE, "Versions",
				new GridLayout(), versionGridData);
		// Version list
		lVersions = SWTUtils.createList(versionGroup, SWT.BORDER | SWT.MULTI
				| SWT.V_SCROLL, versionGridData);

		// Report Group
		GridData reportGroupGridData = new GridData(GridData.FILL_BOTH);
		reportGroupGridData.verticalAlignment = SWT.TOP;
		final Group reportGroup = SWTUtils.createGroup(this, SWT.NONE,
				"Reporting Options", new GridLayout(), reportGroupGridData);
		reportGroup.setBounds(shell.getClientArea());
		
		// FIXME: This makes sure the entire report section expands to fill available
		// area as the window is resized. Causes flickering, should investigate.
//		final Rectangle r = this.getClientArea();
//		shell.addControlListener(new ControlAdapter()
//		{
//			public void controlResized(ControlEvent e)
//			{
//				reportGroup.setBounds(r);
//				reportGroup.redraw();
//			}
//		});

		GridData reportHeaderGridData = new GridData(GridData.FILL_HORIZONTAL);
		reportHeaderGridData.horizontalAlignment = GridData.CENTER;
		Label reportHeader = new Label(reportGroup, SWT.NONE);
		reportHeader.setText("Report Type:");
		reportHeader.setLayoutData(reportHeaderGridData);

		Composite compReportSel = SWTUtils.centerComposite(reportGroup, SWT.NONE);
		cReportSelection = SWTUtils.createCombo(compReportSel, SWT.DROP_DOWN
				| SWT.READ_ONLY, 10, null, new RowData(200, SWT.DEFAULT));

		bReset = SWTUtils.createButton(compReportSel, SWT.PUSH, "Reset", this);
		bReset.setLayoutData(new RowData(50, SWT.DEFAULT));

		Composite compReportConfig = SWTUtils.centerComposite(reportGroup, SWT.NONE);
		cReportConfig = SWTUtils.createCombo(compReportConfig, SWT.DROP_DOWN
				| SWT.READ_ONLY, 10, null, new RowData(150, SWT.DEFAULT));


		bReportBrowse = SWTUtils.createButton(compReportConfig, SWT.PUSH,
				"Browse", this);

		// Table
		table = new Table(reportGroup, SWT.MULTI | SWT.BORDER | SWT.H_SCROLL
				| SWT.V_SCROLL | SWT.HIDE_SELECTION);
		table.setLinesVisible(true);
		table.setHeaderVisible(true);
		table.setToolTipText("Double click to add new entry");

		table.addMouseListener(tableHandler);
		GridData tableGridData = new GridData();
		tableGridData.grabExcessVerticalSpace = true;
		tableGridData.heightHint = shell.getSize().y / 2;
		table.setLayoutData(tableGridData);

		TableColumn argNameCol = new TableColumn(table, SWT.LEFT);
		argNameCol.setText("Name");
		argNameCol.setWidth(100);
		TableColumn argValueCol = new TableColumn(table, SWT.CENTER);
		argValueCol.setText("Value");
		argValueCol.setWidth(100);
		TableColumn removeCol = new TableColumn(table, SWT.RIGHT);
		removeCol.setText("Remove");
		removeCol.setWidth(50);

		pack();

		// Attempt to load the default report file specified in the
		// default configuration file.
		loadReportFile(parent.getProperty(GUI.DEFAULT_REPORTSET));

		cReportSelection.addSelectionListener(this);
		cReportConfig.addSelectionListener(this);
	}

	/**
     * @return The VersionMetricData currently loaded.
     */
	public List getVersions()
	{
		return lVersions;
	}

	/**
     * @return The HistoryMetricData currently loaded.
     */
	public HistoryMetricData getHistoryMetricData()
	{
		return (HistoryMetricData) lVersions.getData();
	}

	/**
     * @return The currently selected report.
     */
	public Report getSelectedReport()
	{
		Report rv = null;
		try
		{
			rv = (Report) cReportSelection.getData(cReportSelection
					.getItem(cReportSelection.getSelectionIndex()));
		} catch (IllegalArgumentException e)
		{
		} // Handle.
		return rv;
	}

	/**
     * @return The configurable part of a report. i.e. It's arguments
     */
	@SuppressWarnings("unchecked")
	public HashMap<String, Object> getReportConfig()
	{
		HashMap<String, Object> h = new HashMap<String, Object>();
		for (int i = 0; i < table.getItemCount(); i++)
		{
			TableItem item = table.getItem(i);
			TableEditor nameEditor = (TableEditor) item.getData("name");
			TableEditor valueEditor = (TableEditor) item.getData("values");
			CCombo nameCombo = (CCombo) nameEditor.getEditor();
			String nameKey = nameCombo.getText();
			if (valueEditor.getEditor() instanceof CCombo)
			{
				CCombo valueCombo = (CCombo) valueEditor.getEditor();

				if (StringUtils.isBoolean(valueCombo.getText()))
				{
					// Try and parse and see if value is actually a bool.
					boolean value = Boolean.parseBoolean(valueCombo.getText());
					h.put(nameKey, value);
				} else if (h.containsKey(nameKey))
				{
					ArrayList<String> list = (ArrayList<String>) h.get(nameKey);
					list.add(valueCombo.getText());
					h.put(nameKey, list);
				} else
				{
					ArrayList<String> list = new ArrayList<String>();
					list.add(valueCombo.getText());
					h.put(nameKey, list);
				}

			} else if (valueEditor.getEditor() instanceof Text)
			{
				Text valueText = (Text) valueEditor.getEditor();
				h.put(nameCombo.getText(), Integer
						.parseInt(valueText.getText()));
				// For the moment we are only allowing numbers
				// so we will assume that is what we have.
			}
		}
		return h;
	}

	public void widgetDefaultSelected(SelectionEvent arg0)
	{
	} // Not interested in this

	@SuppressWarnings("unchecked")
	public void widgetSelected(SelectionEvent event)
	{
		// Open report configuration
		if (event.getSource() == bReportBrowse)
		{
			String[] filterExt = { SupportedFileType.REPORT.getExtension() };
			String[] filterNames = { SupportedFileType.REPORT.getExtensionName() };
			String filterPath = parent.getProperty(GUI.DEFAULT_REPORT_DIR);

			OpenDialog od = new OpenDialog(shell, filterExt, filterNames,
					filterPath);
			String selected = od.open();

			if (selected != null)
			{
				loadReportFile(selected);
			}
		} else if (event.getSource() == cReportSelection)
		{
			String tmp = event.widget.toString();
			tmp = tmp.substring(tmp.indexOf("{") + 1, tmp.length() - 1);
			Report rv = (Report) cReportSelection.getData(tmp);
			// populate table.
			updateTable(rv);

			// modifiable.
		} else if (event.getSource() == cReportConfig)
		{
			updateReportSelection(event.widget.getData(cReportConfig.getText()));
		} else if (event.getSource() == bReset)
		{
			Report rv = (Report) cReportSelection
					.getData(cReportSelection.getText());
			// Reset report and update table.
			rv.reset();
			processedMetrics.clear();
			updateTable(rv);
		}
	}

	/**
     * Updates the table for the specified <code>ReportVisitor</code> All old
     * items are cleared, and new items are readded.
     * 
     * @param rv The ReportVisitor.
     */
	@SuppressWarnings("unchecked")
	private void updateTable(Report rv)
	{
		// Free up old table items.
		for (TableItem item : table.getItems())
			item.dispose();

		reportConfigMap = rv.getArguments();
		// Some reports may not have specified this.
		if (reportConfigMap != null)
		{
			// Populate default items.
			String[] args = new String[rv.getArguments().keySet().size()];
			rv.getArguments().keySet().toArray(args);

			for (String name : args)
			{
				if (reportConfigMap.get(name) instanceof Collection)
				{
					Collection<String[]> c = (Collection<String[]>) reportConfigMap
							.get(name);
					for (int i = 0; i < c.size(); i++)
					{
						TableItem currentItem = new TableItem(table, SWT.NONE);
						tableHandler.createRow(table, currentItem, args,
								StringUtils.indexOf(args, name));
					}
				} else
				{
					TableItem currentItem = new TableItem(table, SWT.NONE);
					tableHandler.createRow(table, currentItem, args,
							StringUtils.indexOf(args, name));
				}
			}
		}
	}

	private void updateReportSelection(Object data)
	{
		if (data instanceof ReportDefinitionRepository)
		{
			ReportDefinitionRepository selected = (ReportDefinitionRepository) data;

			// Clear whatever was in there.
			cReportSelection.removeAll();

			// Sort definitions
			LinkedList<ReportDefinition> sorted = new LinkedList<ReportDefinition>(
					selected.getDefinitions());
			Collections.sort(sorted);

			// Add the current repository and set reports.
			for (ReportDefinition def : sorted)
			{
				cReportSelection.add(def.toString());
				try
				{
					cReportSelection.setData(def.toString(), ReportFactory
							.getReport(def));
				} catch (ReportException e)
				{
					logger.log(Level.ALL, e.getMessage());
//					e.printStackTrace();
				}
			}
			cReportSelection.select(0); // select first.
		}
	}

	/**
     * Starts a <code>ThreadedDefinitionProcessor</code> to create a
     * <code>ReportDefinitionRepository</code> for the specified filename.
     * 
     * @param filename The filename of the *.reports file to load.
     */
	private void loadReportFile(final String filename)
	{
		ThreadedDefinitionProcessor tdp = new ThreadedDefinitionProcessor(
				cReportConfig, filename);
		tdp.start();
	}

	/**
     * Responsible for handling all interaction with the report arguments table.
     * 
     * @author Joshua Hayes,Swinburne University (ICT),2007
     */
	class TableHandler implements MouseListener, SelectionListener
	{
		// private TableItem currentItem;

		/**
         * Creates a new row in the table upon a double click event. The columns
         * used, come from the currently selected report.
         */
		public void mouseDoubleClick(MouseEvent me)
		{
			Report rv = (Report) cReportSelection
					.getData(cReportSelection.getItem(cReportSelection
							.getSelectionIndex()));

			if (rv.getArguments() != null)
			{
				String[] nameArgs = new String[rv.getArguments().keySet()
						.size()];
				rv.getArguments().keySet().toArray(nameArgs);

				// Some reports may not have specified this.
				if (nameArgs != null)
				{
					TableItem currentItem = new TableItem(table, SWT.NONE);
					createRow(table, currentItem, nameArgs, 0);
				}
			}
		}

		public void mouseDown(MouseEvent me)
		{
		} // Not used.

		public void mouseUp(MouseEvent arg0)
		{
		} // Not used.

		public void widgetDefaultSelected(SelectionEvent event)
		{
		} // Not used.

		@SuppressWarnings("unchecked")
		/**
         * When a CCombo widget is selected from the left column. its
         * corresponding value column (middle column) is updated with the
         * appropriate editor according to the type found in the
         * reportConfigMap.
         */
		public void widgetSelected(SelectionEvent event)
		{
			CCombo c = (CCombo) event.getSource();
			String name = c.getText();
			if (name != null && reportConfigMap != null) // Some reports may
			// not have
			// specified this.
			{
				TableEditor editor = (TableEditor) c.getData();

				if (reportConfigMap.get(name) instanceof Integer)
				{
					checkForDispose(editor);
					createTextCell(editor, (Integer) reportConfigMap.get(name));
				} else if (reportConfigMap.get(name) instanceof Boolean)
				{
					checkForDispose(editor);
					String[] values = { "true", "false" };
					createStandardComboCell(editor, values);
				} else
				// Must be string array.
				{
					checkForDispose(editor);
					try
					{
						String[] values = (String[]) reportConfigMap.get(name);
						createStandardComboCell(editor, values);
					} catch (ClassCastException e)
					{
						// Is a Collection<String[]>, not String[]
						Collection<String[]> values = (Collection<String[]>) reportConfigMap
								.get(name);
						for (String[] strings : values)
						{
							if (!processedMetrics.contains(strings))
							{
								createStandardComboCell(editor, strings);
								processedMetrics.add(strings);
								if (processedMetrics.size() == values.size())
									processedMetrics.clear();
								return;
							}
						}
					}
				}
			}
		}

		/**
         * Checks if there is already a table editor in the values position of
         * this table editor. If there is, it is disposed.
         * 
         * @param editor The TableEditor.
         */
		private void checkForDispose(TableEditor editor)
		{
			if (editor.getItem().getData("values") != null)
			{
				TableEditor te = (TableEditor) editor.getItem().getData(
						"values");
				te.getEditor().dispose();
			}
		}

		/**
         * Creates the TableEditor with a CCombo in the first column of the
         * table and a Button (check style) in the third column.
         * 
         * Foces a selection event so that the corresponding values field (2nd
         * column) is updated with the appropriate editor.
         */
		public void createRow(final Table table, final TableItem newItem,
				Object[] values, int selected)
		{
			final CCombo nameCombo = new CCombo(table, SWT.CHECK);
			nameCombo.addSelectionListener(this);

			for (Object object : values)
				nameCombo.add(object.toString());

			nameCombo.select(selected);
			nameCombo.setVisibleItemCount(10);

			/* Set up editor */
			final TableEditor nameEditor = new TableEditor(table);
			nameEditor.horizontalAlignment = SWT.LEFT;
			nameEditor.grabHorizontal = true;
			nameEditor.minimumWidth = 50;
			nameEditor.setEditor(nameCombo, newItem, 0);
			newItem.setData("name", nameEditor);
			nameCombo.setData(nameEditor);

			final Button checkButton = new Button(table, SWT.CHECK);
			checkButton.pack();

			final TableEditor checkEditor = new TableEditor(table);
			checkEditor.horizontalAlignment = SWT.CENTER;
			checkEditor.minimumWidth = checkButton.getSize().x;
			checkEditor.setEditor(checkButton, newItem, 2);

			// Hook in dispose listener to clean up child widgets
			// when TableItem is disposed.
			newItem.addDisposeListener(new DisposeListener()
			{
				public void widgetDisposed(DisposeEvent arg0)
				{
					nameCombo.dispose();
					nameEditor.dispose();
					checkButton.dispose();
					checkEditor.dispose();
				}
			});

			// Listen for checkbutton selection events dispose
			// the main TableItem (which will in turn dispose all
			// the widgets in that row.
			checkButton.addSelectionListener(new SelectionAdapter()
			{
				public void widgetSelected(SelectionEvent arg0)
				{
					newItem.dispose();
//					table.layout(table.getChildren());
				}
			});

			// Fake a selection event to force value to update.
			nameCombo.notifyListeners(SWT.Selection, new Event());
		}

		/**
         * Creates a standard CCombo box cell with the specified values.
         * 
         * @param editor The TableEditor.
         * @param values The values for the combo box.
         */
		private void createStandardComboCell(TableEditor editor, String[] values)
		{
			final CCombo combo = new CCombo(table, SWT.CHECK);
			for (String str : values)
				combo.add(str);

			combo.select(0);
			combo.setVisibleItemCount(10);

			final TableEditor valueEditor = new TableEditor(table);
			valueEditor.horizontalAlignment = SWT.LEFT;
			valueEditor.grabHorizontal = true;
			valueEditor.minimumWidth = 50;
			valueEditor.setEditor(combo, editor.getItem(), 1);
			editor.getItem().setData("values", valueEditor);

			addDisposeListener(editor.getItem(), combo);
		}

		/**
         * Creates a Text cell constrained to only numbers.
         * 
         * @param editor The TableEditor.
         * @param defaultValue The default value that should be displayed.
         */
		private void createTextCell(TableEditor editor, int defaultValue)
		{
			final Text text = new Text(table, SWT.NONE);
			text.setText(String.valueOf(defaultValue));
			// Constrain to only allow numbers input.
			text.addVerifyListener(new VerifyListener()
			{
				public void verifyText(VerifyEvent vEvent)
				{
					vEvent.doit = false;
					if (Character.isDigit(vEvent.character))
						vEvent.doit = true;
				}
			});

			final TableEditor valueEditor = new TableEditor(table);
			valueEditor.horizontalAlignment = SWT.LEFT;
			valueEditor.grabHorizontal = true;
			valueEditor.minimumWidth = 50;
			valueEditor.setEditor(text, editor.getItem(), 1);
			editor.getItem().setData("values", valueEditor);

			addDisposeListener(editor.getItem(), text);
		}

		/**
         * Adds a dispose listener to the specified TableItem for the Specified
         * Widget. This means when a dispose call is made on a Tableitem, all
         * its widgets will be disposed of properly too.
         * 
         * @param item The TableItem
         * @param w The Widget that should be disposed of when the TableItem is
         *            disposed.
         */
		private void addDisposeListener(TableItem item, final Widget w)
		{
			// Hook in dispose listener to clean up child widget
			// when TableItem is disposed.
			item.addDisposeListener(new DisposeListener()
			{
				public void widgetDisposed(DisposeEvent arg0)
				{
					w.dispose();
				}
			});
		}
	}

}
